/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
*************************************************************************/


/**************************************************************************
 * Dependencies
 *************************************************************************/
#include "engine/common.h"
#include "engine/engine.h"
#include "engine/menu.h"
#include "engine/network.h"
#include "engine/render.h"
#include "engine/sound.h"
#include "engine/thread.h"
#include "engine/util.h"

/**************************************************************************
 * Preprocessing
 *************************************************************************/

/**************************************************************************
 * Types
 *************************************************************************/

/**************************************************************************
 * Variables
 *************************************************************************/
const char* APP_ABOUT = "";
const char* APP_TITLE = "Test";
bounds_type app_bounds = {0, 1, 0, 1};

/**************************************************************************
 * Static Variables
 *************************************************************************/
#define PRODUCTION_COUNT 99999
#define NUM_CONSUMERS 8
#define NUM_PRODUCERS 8
static struct
{
  thread_rwlock_type rwlock;
  int num_consumed;
  int num_produced;
  float produced[PRODUCTION_COUNT];
} thread_test_data;

static struct
{
  int port;
  network_protocol_type protocol;
} network_test_data;

static char background_message[256] = {0};
static menu_type main_menu;

/**************************************************************************
 * Function Prototypes
 *************************************************************************/
static void check_float_production(void* data);
static void fail_test(const char* function_name, const char* message);
static void float_consumer(void* data);
static void float_producer(void* data);
static void network_client(void* data);
static void network_server(void* data);
static void test_common(void);
static void test_network(void);
static void test_sound(void);
static void test_thread(void);
static void test_util(void);

/**************************************************************************
 * Function Implementations
 *************************************************************************/
void app_init(void) { }
void app_navigate(navigation_type navigation) { }
void app_render(const bounds_type* bounds) { }
void app_resize(const bounds_type* bounds) { }
void app_update(float secs) { }
BOOL app_session_load(void) { return FALSE; }
void app_session_save(void) { }
void app_settings_save(void) { }
void app_start(void) { }
void app_stop(BOOL won) { }
void app_touch_move(const touch_type* touch) { }
void app_touch_start(const touch_type* touch) { }
void app_touch_stop(const touch_type* touch) { }
void app_uninit(void) { }
void background_init(void) { }
void menu_pause(void) { }

static void check_float_production(void* data)
{
  BOOL done = FALSE;
  while (!done) {
    thread_lock_read(&(thread_test_data.rwlock));
    done = (thread_test_data.num_consumed >= PRODUCTION_COUNT);
    thread_unlock_read(&(thread_test_data.rwlock));
  }
}

static void fail_test(const char* function_name, const char* message)
{
  fprintf(stderr, "\n*****************************************\n");
  fprintf(stderr, "function \"%s\" failed: %s\n", function_name, message);
  fprintf(stderr, "*****************************************\n");

  log_message(LOG_LEVEL_ERROR, "function \"%s\" failed: %s", function_name, message);

  engine_exit(-1);
}

static void float_consumer(void* data)
{
  int id = *((int*)data);
  BOOL done = FALSE;

  log_message(LOG_LEVEL_DEBUG, "consumer thread %2d created.", id);

  while (!done) {
    thread_lock_write(&(thread_test_data.rwlock));
    while (thread_test_data.num_consumed < thread_test_data.num_produced) {
      //log_message(LOG_LEVEL_DEBUG, "consumer thread %2d consumed %8d [%1.8f]", id,
      //            thread_test_data.num_consumed,
      //            thread_test_data.produced[thread_test_data.num_consumed]);
      thread_test_data.num_consumed++;
    }
    done = (thread_test_data.num_consumed >= PRODUCTION_COUNT);
    thread_unlock_write(&(thread_test_data.rwlock));
  }
}

static void float_producer(void* data)
{
  int id = *((int*)data);
  BOOL done = FALSE;

  log_message(LOG_LEVEL_DEBUG, "producer thread %2d created.", id);

  while (!done) {
    thread_lock_write(&(thread_test_data.rwlock));
    if (thread_test_data.num_produced < PRODUCTION_COUNT) {
      thread_test_data.produced[thread_test_data.num_produced] = random_float(0, 1);
      //log_message(LOG_LEVEL_DEBUG, "producer thread %2d produced %8d [%1.8f]", id,
      //            thread_test_data.num_produced,
      //            thread_test_data.produced[thread_test_data.num_produced]);
      thread_test_data.num_produced++;
    } else {
      done = TRUE;
    }
    thread_unlock_write(&(thread_test_data.rwlock));
  }
}

static void network_client(void* data)
{
  network_type network;
  char buffer[256];
  int length;
  int bytes_sent;

  sprintf(buffer, "client protocol:%d port:%d",
          network_test_data.protocol, network_test_data.port);

  if (!network_create(&network, network_test_data.protocol)) {
    fail_test("network_create", buffer);
  }

  if (!network_connect(&network, "127.0.0.1", network_test_data.port)) {
    fail_test("network_connect", buffer);
  }

  length = (int)strlen(buffer);
  bytes_sent = 0;
  while (bytes_sent < sizeof(length)) {
    bytes_sent = network_send(&network, (UCHAR*)&length, sizeof(length));
    if (bytes_sent < 0) {
      fail_test("network_send", "sending message length");
    }
  }

  bytes_sent = 0;
  while (bytes_sent < length) {
    bytes_sent = network_send(&network, (UCHAR*)buffer, length);
    if (bytes_sent < 0) {
      fail_test("network_send", "sending message");
    }
  }
 
  log_message(LOG_LEVEL_DEBUG, "network client sent \"%s\"", buffer);

  network_close(&network);
}

static void network_server(void* data)
{
  network_type network;
  network_type client;
  char buffer[256];
  int length;
  int bytes_received;

  sprintf(buffer, "server protocol:%d port:%d",
          network_test_data.protocol, network_test_data.port);

  if (!network_create(&network, network_test_data.protocol)) {
    fail_test("network_create", buffer);
  }

  if (!network_bind(&network, network_test_data.port)) {
    fail_test("network_bind", buffer);
  }

  if (network_test_data.protocol == NETWORK_PROTOCOL_TCP) {
    if (!network_wait_for_client(&network, &client)) {
      fail_test("network_wait_for_client", buffer);
    }
  }

  // wait for length
  bytes_received = 0;
  while (bytes_received < sizeof(length)) {
    bytes_received = network_receive(&network, &client, sizeof(length), (UCHAR*)&length);
    if (bytes_received < 0) {
      fail_test("network_receive_from", "receiving message length");
    }
  }

  // wait for data
  bytes_received = 0;
  while (bytes_received < length) {
    bytes_received = network_receive(&network, &client, length, (UCHAR*)buffer);
    if (bytes_received < 0) {
      fail_test("network_receive_from", "receiving message");
    }
  }

  log_message(LOG_LEVEL_DEBUG, "network server received \"%s\"", buffer);

  network_close(&client);
  network_close(&network);
}

static void test_common(void)
{
  char buffer[MAX_DATE_LENGTH];
  char path[MAX_PATH_LENGTH];

  log_message(LOG_LEVEL_INFO, "Common test starting...");

  if (IS_LITTLE_ENDIAN) {
    log_message(LOG_LEVEL_INFO, "Machine is little endian.");
  } else {
    log_message(LOG_LEVEL_INFO, "Machine is big endian.");
  }

  current_date(buffer);
  log_message(LOG_LEVEL_INFO, "Current Date: %s", buffer);

  log_message(LOG_LEVEL_INFO, "Elapsed Seconds: %1.4f", elapsed_seconds());

  get_data_dir(path);
  log_message(LOG_LEVEL_INFO, "Data Directory: %s", path);

  get_resource_dir(path);
  log_message(LOG_LEVEL_INFO, "Resource Directory: %s", path);

  log_message(LOG_LEVEL_INFO, "Common test passed!");
}

static void test_network(void)
{
  thread_type client_thread[NETWORK_PROTOCOL_COUNT];
  thread_type server_thread[NETWORK_PROTOCOL_COUNT];
  int i;

  log_message(LOG_LEVEL_INFO, "Network test starting...");

  network_test_data.port = 56789;
  for (i = 0; i < NETWORK_PROTOCOL_COUNT; i++) {
    network_test_data.protocol = (network_protocol_type)i;
    thread_create(network_server, NULL, TRUE, &(server_thread[i]));

    // give server some time to prepare for clients
    thread_sleep(1000);

    thread_create(network_client, NULL, TRUE, &(client_thread[i]));

    thread_wait(&(client_thread[i]));
    thread_wait(&(server_thread[i]));
  }

  log_message(LOG_LEVEL_INFO, "Network test passed!");
}

static void test_sound(void)
{
  log_message(LOG_LEVEL_INFO, "Sound test starting...");

  if (sound_is_playing(&sound_cheering)) {
    fail_test("sound_is_playing", "thought a sound was playing when it wasn't.");
  }

  if (!sound_play(&sound_cheering, TRUE)) {
    fail_test("sound_play", "");
  }

  thread_sleep(1000);
 
  if (!sound_is_playing(&sound_cheering)) {
    fail_test("sound_is_playing", "thought a sound wasn't playing when it was.");
  }

  thread_sleep(1000);
 
  if (!sound_pause(&sound_cheering)) {
    fail_test("sound_pause", "");
  }

  thread_sleep(1000);

  if (!sound_play(&sound_cheering, TRUE)) {
    fail_test("sound_play", "could not resume a sound");
  }

  thread_sleep(1000);
 
  if (!sound_stop(&sound_cheering)) {
    fail_test("sound_stop", "");
  }

  log_message(LOG_LEVEL_INFO, "Sound test passed!");
}

static void test_thread(void)
{
  thread_type consumer_threads[NUM_CONSUMERS];
  thread_type producer_threads[NUM_PRODUCERS];
  thread_type status_thread;
  float start_time_secs;
  int consumer_threads_idx[NUM_CONSUMERS], producer_threads_idx[NUM_PRODUCERS];
  int i;

  log_message(LOG_LEVEL_INFO, "Thread test starting...");

  randomize();

  memset(&thread_test_data, 0, sizeof(thread_test_data));
  thread_create_rwlock(&(thread_test_data.rwlock));

  // create consumers
  for (i = 0; i < NUM_CONSUMERS; i++) {
    consumer_threads_idx[i] = i;
    thread_create(float_consumer, &(consumer_threads_idx[i]), FALSE, &(consumer_threads[i]));
  }

  // create producers
  for (i = 0; i < NUM_PRODUCERS; i++) {
    producer_threads_idx[i] = i;
    thread_create(float_producer, &(producer_threads_idx[i]), FALSE, &(producer_threads[i]));
  }

  // wait for the production to end and time it
  start_time_secs = elapsed_seconds();
  thread_create(check_float_production, NULL, TRUE, &status_thread);
  thread_wait(&status_thread);
  log_message(LOG_LEVEL_DEBUG, "Producing[%d] & Consuming[%d] %d random floats took %1.4f seconds",
              NUM_PRODUCERS, NUM_CONSUMERS, PRODUCTION_COUNT,
              (elapsed_seconds() - start_time_secs));

  log_message(LOG_LEVEL_INFO, "Thread test passed!");
}

static void test_util(void)
{
  float v1[3] = {1, 0, 0};
  float v2[3] = {0, 1, 0};
  float v3[3] = {0, 0, 1};
  float v4[3] = {0, 0, 0};
  const char* ENCRYPTION_TEST = "This is a test string for encryption.";
  char buffer[256];
  float x, y, z, r, theta, rho, phi, new_x, new_y, new_z;
  float m1[16], m2[16], m3[16];
  float t[2][3] = {
    {1, 2, 3},
    {1, 2, 3},
  };
  float t_trans[3][2];
  float u[2][1] = {
    {-1},
    {1},
  };
  float w[3][1] = {
    {1},
    {2},
    {3},
  };
  int ints[3];
  int i, j;

  log_message(LOG_LEVEL_INFO, "Util test starting...");

  if (!_CLOSE_ENOUGH(angle_between((float*)v1, (float*)v2), (PI / 2.0))) {
    fail_test("angle_between", "");
  }

  if (!_CLOSE_ENOUGH(change_scale(1, 0, 10, 0, 100), 10) ||
      !_CLOSE_ENOUGH(change_scale(-1, 0, 10, 0, 100), 0)) {
    fail_test("change_scale", "");
  }

  if (!_CLOSE_ENOUGH(change_scale_without_containing(-1, 0, 10, 0, 100), -10)) {
    fail_test("change_scale_without_containing", "");
  }

  set_encryption_key(ENCRYPTION_TEST);
  strcpy(buffer, ENCRYPTION_TEST);
  if (!encrypt_block((UCHAR*)buffer, (int)strlen(ENCRYPTION_TEST)) ||
      (strcmp(buffer, ENCRYPTION_TEST) == 0)) {
    fail_test("encrypt_block", "");
  }
  if (!decrypt_block((UCHAR*)buffer, (int)strlen(ENCRYPTION_TEST)) ||
      (strcmp(buffer, ENCRYPTION_TEST) != 0)) {
    fail_test("decrypt_block", "");
  }

  for (i = 0; i < 16; i++) {
    randomize();
    v1[X] = x = random_float(-100, 100);
    v1[Y] = y = random_float(-100, 100);
    v1[Z] = z = random_float(-100, 100);

    convert_to_polar(x, y, &r, &theta);
    convert_polar(r, theta, &new_x, &new_y);
    if (!_CLOSE_ENOUGH(x, new_x) || !_CLOSE_ENOUGH(y, new_y)) {
      fail_test("convert_to_polar/convert_polar", "");
    }

    convert_to_spherical(x, y, z, &rho, &phi, &theta);
    convert_spherical(rho, phi, theta, &new_x, &new_y, &new_z);
    if (!_CLOSE_ENOUGH(x, new_x) || !_CLOSE_ENOUGH(y, new_y) || !_CLOSE_ENOUGH(z, new_z)) {
      fail_test("convert_to_spherical/convert_spherical", "");
    }

    if (!normalize((float*)v1) || !_CLOSE_ENOUGH(vector_length((float*)v1), 1)) {
      fail_test("normalize/vector_length", "");
    }

    for (j = 0; j < 3; j++) {
      v3[j] = (v2[j] - v1[j]);
    }
    if (!_CLOSE_ENOUGH(get_distance((float*)v1, (float*)v2, 3),
                       vector_length((float*)v3))) {
      fail_test("get_distance/vector_length", "");
    }

    sort_float((float*)v1, 3, TRUE);
    if ((v1[0] > v1[1]) || (v1[1] > v1[2])) {
      fail_test("sort_float", "");
    }

    for (j = 0; j < 2; j++) {
      v3[j] = v1[j];
      v4[j] = v1[j];
    }
    v3[Z] = v4[Z] = 0;
    rotate_point(&(v4[X]), &(v4[Y]), random_int(0, 1));
    if (!_CLOSE_ENOUGH(dot_product((float*)v3, (float*)v4), 0)) {
      fail_test("rotate_point/dot_product", "");
    }

    for (j = 0; j < 3; j++) {
      ints[j] = round_float(v1[j]);
      v2[j] = v1[j];
    }
    sort_int((int*)ints, 3, FALSE);
    if ((ints[0] < ints[1]) || (ints[1] < ints[2])) {
      fail_test("sort_int", "");
    }
 
    for (j = 0; j < 16; j++) {
      m1[j] = random_float(-1, 1);
    }

    if (matrix_invert_4((float*)m1, (float*)m2)) {
      // matrix inverse exists
      matrix_multiply_44_44((float*)m1, (float*)m2, (float*)m3);
      if (!matrix_equal((float*)m3, (float*)MATRIX_IDENTITY_4, 4, 4)) {
        fail_test("matrix_invert_4/matrix_multiply_44_44/matrix_equal", "");
      }
      matrix_multiply_44_44((float*)m2, (float*)m1, (float*)m3);
      if (!matrix_equal((float*)m3, (float*)MATRIX_IDENTITY_4, 4, 4)) {
        fail_test("matrix_invert_4/matrix_multiply_44_44/matrix_equal", "");
      }
    }
  }

  // irregular matrix multiplication
  matrix_transpose((float*)t, 2, 3, (float*)t_trans);
  matrix_multiply((float*)t_trans, (float*)u, 3, 2, 1, (float*)w);
  for (i = 0; i < 3; i++) {
    if (!_CLOSE_ENOUGH(w[i][0], 0)) {
      fail_test("matrix_transpose/matrix_muliply", "");
    }
  }

  if ((round_float(-0.9) != -1) || (round_float(-0.5) != -1) ||
      (round_float(-0.1) != 0) || (round_float(0.1) != 0) ||
      (round_float(0.5) != 1) || (round_float(0.9) != 1)) {
    fail_test("round_float", "");
  }

  if (file_get_extension("bad_file_name", buffer) ||
      !file_get_extension("good_file_name.PnG", buffer) ||
      (strcmp(buffer, "png") != 0)) {
    fail_test("file_extension", "");
  }

  log_message(LOG_LEVEL_INFO, "Util test passed!");
}

void background_render(const bounds_type* bounds)
{
  render_set_view_ortho(bounds);
  render_button(bounds, background_message, &OVERLAY_COLOR, &(ACTIVE_TEXT_COLORS[FALSE]));
}

void background_update(float secs)
{
  static float delay_secs = 2.0;
  static int stage = 0;
 
  level_logged = LOG_LEVEL_DEBUG;

  switch (stage) {
  case 0:
    delay_secs -= secs;

    if (delay_secs > 0) {
      sprintf(background_message, "Test starting in %d seconds...", round_float(delay_secs));
      return;
    }
    strcpy(background_message, "Testing common...");
    break;
  case 1:
    log_message(LOG_LEVEL_INFO, "Testing common...");
    test_common();
    strcpy(background_message, "Testing util...");
    break;
  case 2:
    log_message(LOG_LEVEL_INFO, "Testing util...");
    test_util();
    strcpy(background_message, "Testing thread...");
    break;
  case 3:
    log_message(LOG_LEVEL_INFO, "Testing thread...");
    test_thread();
    strcpy(background_message, "Testing sound...");
    break;
  case 4:
    log_message(LOG_LEVEL_INFO, "Testing sound...");
    test_sound();
#if defined(NETWORK_STANDARD) || defined(NETWORK_WIN)
    strcpy(background_message, "Testing network...");
    break;
  case 5:
    log_message(LOG_LEVEL_INFO, "Testing network...");
    test_network();
#endif
    strcpy(background_message, "Testing complete!");
    break;
  case 6:
    log_message(LOG_LEVEL_INFO, "Test passed!");
    break;
  default:
    engine_quit(TRUE, 0);
    return;
  }

  stage += 1;
}

void menu_open(void)
{
  void* args[MAX_ACTION_ARGS];
  args[0] = &menu_nav_forward;
  args[1] = &main_menu;
  menu_goto(args);
}

void menu_setup(void)
{
  // setup main menu
  memset(&main_menu, 0, sizeof(menu_type));
  strcpy(main_menu.title, "Hawkengine Test");
}
